package com.adee.serializer.jdk;

import com.alibaba.fastjson2.JSON;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
import org.junit.Test;

import java.io.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * JDK序列化
 * JDK Serializable是Java自带的序列化框架，我们只需要实现java.io.Serializable或java.io.Externalizable接口，
 * 就可以使用Java自带的序列化机制。实现序列化接口只是表示该类能够被序列化/反序列化，我们还需要借助I/O操作
 * 的ObjectInputStream和ObjectOutputStream对对象进行序列化和反序列化。
 *
 * 通用性
 *
 * 由于是Java内置序列化框架，所以本身是不支持跨语言序列化与反序列化。
 *
 * 易用性
 *
 * 作为Java内置序列化框架，无需引用任何外部依赖即可完成序列化任务。但是JDK Serializable在使用上相比开源框架难用许多，
 * 可以看到上面的编解码使用非常生硬，需要借助ByteArrayOutputStream和ByteArrayInputStream才可以完整字节的转换。
 *
 * 下面是使用JDK 序列化框架进行编解码的Demo：
 *
 * 测试步骤：
 * 1.先执行test1方法，将对象序列化到文件中；
 * 2.修改序列化类Student的serialVersionUID；
 * 3.读取文件中字节数组，反序列化，验证serialVersionUID不一致时抛出异常：
 *   java.io.InvalidClassException: com.adee.serializer.jdk.TestJdkSerializer$Student; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
 */
public class TestJdkSerializer {

    static final String  path = "temp.bin";

    private static Student student;

    static {
        Map<String,Object> hobbies = new HashMap<>();
        hobbies.put("游泳", "蛙泳自学中...");
        hobbies.put("跑步", "跑一休一");

        List<Teacher> teachers = new ArrayList<>();
        teachers.add(new Teacher(1,"王老师"));
        teachers.add(new Teacher(5, "陈老师"));
        // !!!不能使用如下这种匿名类，Kryo不支持匿名类，反序列化时会报错
        // Arrays.asList(new Teacher(1,"王老师"),new Teacher(5, "陈老师"));

        student = new Student.StudentBuilder()
                .id(1)
                .name("张三")
                .teachers(teachers)
                .hobbies(hobbies)
                .build();
    }

    public static void main(String[] args) throws Exception {

        System.out.println("序列化之前：");
        System.out.println(JSON.toJSONString(student));
        byte[] bytes = encoder(student);

        System.out.println("反序列化之后：");
        Student student2 = decoder(bytes, Student.class);
        System.out.println(JSON.toJSONString(student2));
    }

    // 序列化
    @Test
    public void test1() throws Exception {

        System.out.println("序列化之前：");
        System.out.println(JSON.toJSONString(student));
        byte[] bytes = encoder(student);
        // 序列化字节数组写入文件
        writeBytesToFile("temp.bin", bytes);
    }

    // 反序列化
    @Test
    public void test2() throws Exception {

        // ... 修改Student类的serialVersionUID
        byte[] bytes = readBytesFromFile(path);
        System.out.println("反序列化之后：");
        Student student2 = decoder(bytes, Student.class);
        System.out.println(JSON.toJSONString(student2));
    }


    // 性能测试
    @Test
    public void test3() throws Exception {
        // 序列化、反序列化 100w次
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            byte[] bytes = encoder(student);
            Student student2 = decoder(bytes, Student.class);
        }
        long millis = System.currentTimeMillis() -begin;
        System.out.println("jdk耗时：" + millis); // jdk耗时：24238

    }

    /**
     * 编码
     */
    public static byte[] encoder(Object ob) throws Exception{
        //用于缓冲字节数字
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        //序列化对象
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(ob);
        byte[] result = byteArrayOutputStream.toByteArray();
        //关闭流
        objectOutputStream.close();
        byteArrayOutputStream.close();
        return result;
    }
    /**
     * 解码
     */
    public static <T> T decoder(byte[] bytes, Class<T> tClass) throws Exception {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        T object = (T) objectInputStream.readObject();
        objectInputStream.close();
        byteArrayInputStream.close();
        return object;
    }

    // 向文件中写入字节数组
    public static void writeBytesToFile(String path, byte[] bytes) throws IOException {
        URL resource = TestJdkSerializer.class.getClassLoader().getResource("");
        String rootDir = resource.getPath();
        File tempFile = new File(rootDir + "/" + path);
        // 清空文件：先删除，后创建
        if (tempFile.exists()) {
            tempFile.delete();
        }
        tempFile.createNewFile();
        RandomAccessFile rf = new RandomAccessFile(tempFile.getAbsolutePath(), "rw");
        rf.write(bytes);
        rf.close();
    }

    // 从文件中读取字节数组
    public static byte[] readBytesFromFile(String path) throws IOException {
        URL resource = TestJdkSerializer.class.getClassLoader().getResource("");
        String rootDir = resource.getPath();
        RandomAccessFile rf = new RandomAccessFile(rootDir + "/" + path, "r");
        byte[] bytes = new byte[(int)rf.length()];
        rf.read(bytes);
        rf.close();
        return bytes;
    }

    @Data
    @Accessors(chain = true)
    @Builder
    public static class Student implements Serializable {

        /**
         * serialVersionUID的作用？
         * 答：serialVersionUID 是Java序列化机制中使用的一个版本号，用来控制对象反序列化时的兼容性。
         * 在Java中，当一个对象被序列化后，如果在序列化和反序列化之间，对该对象的结构进行了修改，那么
         * 反序列化就会失败，这时候就需要通过指定 serialVersionUID 来指定兼容版本。 序列化机制会使用
         * 类的serialVersionUID来验证版本一致性。在进行反序列化时，JVM会把传来的字节流中的serialVersionUID
         * 与本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的，可以进行反序列化，否则就会出现
         * 不兼容版本异常。 如果一个类没有定义 serialVersionUID ，那么在程序中修改了该类的结构后，就会导致
         * 反序列化失败。为了解决这个问题，可以在类中定义一个固定的 serialVersionUID 值。在序列化和反序列化
         * 的时候，JVM会自动识别该值，并进行版本的比较，从而确保只有同一版本的类才能进行反序列化。因此，为了保证
         * 序列化和反序列化的兼容性，一般建议在所有需要序列化的类中定义 serialVersionUID 值。
         *
         */
        public static final long serialVersionUID = 2L;

        private long id;
        private String name;

        private List<Teacher> teachers;

        private Map<String,Object> hobbies;
    }

    @AllArgsConstructor
    public static class Teacher implements Serializable {
        public static final long serialVersionUID = 1L;

        private long id;
        private String name;
    }
}
